Simulating Object Oriented Programming in Visual Basic Compuserve Text posted by Pete Washburn (73750,3141) January 1994 Here's some of the techniques I've used to emulate an OOP language with QB and VB. I must add a couple of comments however. I'm not saying that Basic is an OOP, but with a few techniques, you can get most of the principles of OOP into Basic. These are just a few of techniques I've used. They may not be the most efficient or optimum methods available, but they seem to be working for me. Improvements are always accepted. Debates about the finer parts of OOP theory are not! I do a fair amount of work with system simulation, so I'll use a segment of one of my projects to demonstrate. Most OOP's contain a class tree to define the behavior of objects within the program. In this demo case, we are modeling a plumbing system, with pipes, hoses, and valves. The class tree may look as follows: WaterPart Pipe Hoses Valves Pumps We'll look at the WaterPart class. It is defined as a Function as follows: Function WaterPart (hObj, msg, value) ' all data for objects of this type is contained within the Function, ' within the following instance array Static WaterPartInstances() As waterPartObject ' count of individual objects of this type Static WaterPartsCount% ' using the hObj passed, find the object's data in the Instance array ' (there are a lot more sophisticated and efficient ways to do this, ' used as an example only) For idx = 1 To WaterPartsCount% If WaterPartsInstances(idx).hObj = hObj Then Exit For End If Next idx ' methods Select Case msg Case GET_QUANTITY WaterPart = WaterPartInstances(idx).quantity Case SET_QUANTITY If value <> WaterPartInstances(idx).quantity Then If WaterPartInstances(idx).diameter = 0 Then value = 0 End If WaterPartInstances(idx).quantity = value End If WaterPart = WaterPartInstances(idx).quantity Case GET_DIAMETER WaterPart = WaterPartInstances(idx).diameter Case SET_DIAMETER If value <> WaterPartInstances(idx).diameter Then WaterPartInstances(idx).diameter = value WaterPartInstances(idx).area = .7854 * value ^ 2 End If WaterPart = WaterPartInstances(idx).diameter Case GET_AREA WaterPart = WaterPartInstances(idx).area Case NEW_OBJECT WaterPartsCount% = WaterPartsCount% + 1 idx = WaterPartsCount% WaterPartInstances(idx).hObj = hObj WaterPart = Self(hObj, INIT_OBJECT, value) Case INIT_OBJECT WaterPart = Self(hObj, SET_DIAMETER, value) Case DISPLAY_OBJECT ' code to display the part Case READ_OBJECT ' code to read info about the part from a file Case WRITE_OBJECT ' code to write part info to a file Case PRINT_OBJECT ' code to print the part to a printer Case INIT_CLASS ReDim WaterPartInstances(1 To MAX_WATERPARTS) As waterPartObject Case Else a = "Object Doesn't Understand Message." + Chr$(10) + Chr$(10) a = a + "Class: Part" + Chr$(10) a = a + "Object: " + Str$(hObj) + Chr$(10) a = a + "Message: " + Str$(msg) + Chr$(10) a = MsgBox(a, 16, "Message Error") End Select End Function Because VB won't let us define user Types within a Function, the Declarations section of our program describes the Type variable that will contain the object's information. Type waterPartObject hObj as Integer diameter As Single area As Single quantity As Integer End Type Also, constants used by our VB program must be declared in the Declarations section. ' define messages sent to objects Global Const NEW_OBJECT = 1 Global Const INIT_OBJECT = 2 Global Const DISPLAY_OBJECT = 3 Global Const READ_OBJECT = 4 Global Const WRITE_OBJECT = 5 Global Const INIT_CLASS = 6 Global Const GET_QUANTITY = 100 Global Const SET_QUANTITY = 101 Global Const GET_DIAMETER = 102 Global Const SET_DIAMETER = 103 Global Const GET_AREA = 104 ' define max number of water parts Global Const MAX_WATERPARTS = 30 A language is considered object oriented if it supports three major features: 1. Encapsulation (or information and implementation hiding) 2. Inheritance. 3. Polymorphism The Function helps us with Encapsulation. Both the data and the methods that work with the data are contained solely within the Function. The Type we defined contains all of the instance variables of an object. An array is created to hold the instance variables of each object of the class. There are many different ways to find the instance data within the array for a specific object. In this example, we simply do a brute search for the key, hObj within the array. The data is not globally defined, so the only way to access an object's data is by the methods that are defined within the Function. We work with an object by passing the handle of the object (hObj) to the Function along with the message that we want and any additional we need to provide. To get the area of the particular part, anObj, we would send the following: area = WaterPart(anObj, GET_AREA, Null) Similarly, to set a new diameter, we would send the following message: returnValue = WaterPart(anObj, SET_DIAMETER, 4.55) One difference between this technique with VB and a real OOP is that the number of parameters sent to a Function is constant. In a real OOP, each method would be defined separately, with the specific number of parameters it needed. Because we're wrapping our class and all of its methods within one Function, the number of parameters is fixed. Therefore, each message consist of three parameters, even if you don't need all three. Simply send a Null for any parameter that isn't needed. Likewise, there might be some cases that need more than the three parameters specified. With QB, I had defined the third parameter as a string and encoded all information I needed into that string. The Function then parsed out the info as it needed it. VB is a lot more flexible in this regard. I haven't tried it yet, but by defining the third parameter as being of the Variant type, I believe you can send any type of variable you wish as the third parameter. Perhaps even a user defined Type or array could be sent as the third parameter. Likewise, QB required that you defined the variable type that the Function returned. As a result, I defined all the class functions as being strings. That way, I could encode any variables being returned from the class function into the string. A VB Function again is more flexible, as it doesn't require you to define the variable type it is, and you can return any type of variable you want back from the function. All of this allows us the advantages of Encapsulation and information and implementation hiding that OOP's normally provide with VB. The second major feature of an OOP is inheritance. We can easily model that within VB. Let's define another class, Pipe, that is a descendant of the WaterPart class we've already defined. Function Pipe(hObj, msg, value) Static pipeInstances() As pipeObject Static pipeCount% ' find instance data for this object For idx = 1 To hoseCount% If pipeInstances(idx).hObj = hObj Then Exit For End If Next idx ' methods Select Case msg Case GET_LENGTH Pipe = pipeInstances(idx).length Case SET_LENGTH If value <> pipeInstances(idx).length Then pipeInstances(idx).length = value pipeInstances(idx).volume = Pipe(hObj, GET_DIAMETER, Null) End If Pipe = pipeInstances(idx).length Case GET_VOLUME Pipe = pipeInstances(idx).volume Case SET_DIAMETER diameter = WaterPart(hObj, msg, value) pipeInstances(idx).volume = diameter * pipeInstances(idx).length Case Else ' message not handled by this class, pass on to ancestor Pipe = WaterPart(hObj, msg, value) End Select End Function This has been simplified quite a lot, but basically, the only difference Pipe objects have from the parent class, WaterPart is that Pipes have a length in addition to a diameter. Therefore this class has to process all info in regards to the length of the Pipe. Note the Case Else statement though. If the message hasn't been handled by this class, it is passed on to it's ancestor, which is WaterPart in this case. Note that in the WaterPart class function, the Case Else statement has an error trap as the WaterPart class doesn't have an ancestor class, it is a top level class. Notice that the Pipe class has a SET_DIAMETER method. This overrides the ancestor WaterPart classes method. Actually, it does call it first and then recalculates the volume within the pipe. This brings up a very significant point, if the class sends a message to itself, it needs to be concerned that any overridden methods in its descendants get a chance to process the message first. This is handled within my OOP techniques by calling a special function, Self. Notice in the WaterPart class, that both NEW_OBJECT and INIT_OBJECT send messages to Self. Here's the code for the Self object. ' route a message to the appropriate class of the object Function Self (hObj, msg, value) ' find the class of the object by looking it up in the object ' class array Select Case objectClass%(hObj) Case WATERPART_CLASS Self = WaterPart(hObj, msg, value) Case PIPE_CLASS Self = Pipe(hObj, msg, value) End Select End Function objectClass%() is an array that contains an entry for each object in the program designating what class the object is. Self() simply redirects the message to the appropriate class for the object specified by hObj. Here are the class designators as listed in the Declarations section of the program. ' class identifiers Global Const WATERPART_CLASS = 1 Global Const PIPE_CLASS = 2 This gives us most of the late binding features of OOP's. Its not as elegant or automatic as it is in most OOP's, but it does work. Just make sure when you create an object, you list its class in the objectClass%() array. A similar array could also be created to list the ancestor class of the object. The last major characteristic to be modeled is polymorphism. This means that there might be several messages that are system wide and that all (or most) classes can respond to. In our example, messages such as NEW_OBJECT, INIT_OBJECT, DISPLAY_OBJECT, READ_OBJECT, WRITE_OBJECT, PRINT_OBJECT, and INIT_CLASS are examples of polymorphic messages that most classes implement. The sender of the message doesn't need to know about the class, it simply sends the message. The class handles the details. The Self() function is again used to when sending a message to an object in these cases, as the sender doesn't know even the class of the object that it is sending the message to. Well, that's most of the techniques I've developed for making QB and VB more like a pure OOP. I'm not proposing these techniques as being the best or only way to program! They are simply the techniques that I have developed to provide most of the features I had with Actor in QB and VB. As I work with them, I am refining them and making them more efficient. But the overhead and code to make it work is more complex than it would be if this was a pure OOP.